Plotting data interactively with GeoPandas, Folium, and Leaflet maps

In [1]:
name = '2015-12-14-geopandas_folium'
title = "Plotting a GeoDataFrame with folium"
In [2]:
%matplotlib inline

import os
from datetime import datetime
from IPython.core.display import HTML

import warnings
warnings.simplefilter("ignore")

hour = datetime.utcnow().strftime('%H:%M')
comments="true"

date = '-'.join(name.split('-')[:3])
slug = '-'.join(name.split('-')[3:])

metadata = dict(title=title,
                date=date,
                hour=hour,
                comments=comments,
                slug=slug,
                name=name)

markdown = """Title: {title}
date:  {date} {hour}
comments: {comments}
slug: {slug}

{{% notebook {name}.ipynb cells[2:] %}}
""".format(**metadata)

content = os.path.abspath(os.path.join(os.getcwd(),
                                       os.pardir,
                                       os.pardir, 
                                       '{}.md'.format(name)))
with open('{}'.format(content), 'w') as f:
    f.writelines(markdown)
    

The visualization of thematic maps can get very messy very quick when there are many points to plot display.

In this post we will plot data from shapefile in the most visually efficient way possible.

Let's open our shapefiles with geopandas. (GeoPandas makes our task easy and that will be clear in a moment.)

In [3]:
import geopandas

path = './data/{}'.format
bus = geopandas.GeoDataFrame.from_file(path('metro_bus.shp'))
metro = geopandas.GeoDataFrame.from_file(path('Metro_Station_Entrances_District.shp'))

We need to know the Coordinate Reference System (CRS) of the data. That information is stored in the .crs property and we can use pyepsg to get more information about it.

In [4]:
import pyepsg

pyepsg.get(metro.crs['init'].split(':')[1])
Out[4]:
<GeodeticCRS: 4326, WGS 84>

In order to plot our data we need to make sure that we are in a geographic coordinate system and create a GeoJSON object out of the GeoDataFrame. Luckily, geopandas makes that extremely easy with the to_crs() method and, chained with the to_json(), we have an object ready for plotting with just one line.

In [5]:
gjson = metro.to_crs(epsg='4326').to_json()
In [6]:
import folium

mapa = folium.Map([38.904722, -77.016389],
                  zoom_start=11,
                  tiles='cartodbpositron')

points = folium.features.GeoJson(gjson)

mapa.add_children(points)
mapa
Out[6]:

It is very hard to make sense of what we are plotting. The information is cluttered and has no description.

To make it better we can add more information as rich HTML popups. And to reduced the clutter we can use the plugin MarkerCluster. Since we have to groups of data to plot we can also use FeatureGroup to turn on/off the display of a specific group.

In [7]:
table = """
<!DOCTYPE html>
<html>
<head>
<style>
table {{
    width:100%;
}}
table, th, td {{
    border: 1px solid black;
    border-collapse: collapse;
}}
th, td {{
    padding: 5px;
    text-align: left;
}}
table#t01 tr:nth-child(odd) {{
    background-color: #eee;
}}
table#t01 tr:nth-child(even) {{
   background-color:#fff;
}}
</style>
</head>
<body>

<table id="t01">
  <tr>
    <td>Type</td>
    <td>{}</td>
  </tr>
  <tr>
    <td>Name</td>
    <td>{}</td>
  </tr>
  <tr>
    <td>Entrance</td>
    <td>{}</td>
  </tr>
</table>
</body>
</html>
""".format
In [8]:
mapa = folium.Map([38.904722, -77.016389],
                  zoom_start=11,
                  tiles='cartodbpositron')
In [9]:
from folium.plugins import MarkerCluster

width, height = 310,110
popups, locations = [], []
for idx, row in metro.iterrows():
    locations.append([row['geometry'].y, row['geometry'].x])
    name = row['NAME']
    entrance = row['EXIT_TO_ST']
    iframe = folium.IFrame(table('Metro Stations', name, entrance), width=width, height=height)
    popups.append(iframe)
    
h = folium.FeatureGroup(name='Metro Stations')
h.add_children(MarkerCluster(locations=locations, popups=popups))
mapa.add_children(h)
Out[9]:
In [10]:
popups, locations = [], []
for idx, row in bus.iterrows():
    locations.append([row['LATITUDE'], row['LONGITUDE']])
    cross_st = row['AT_STR']
    route_st = row['ON_STR']
    iframe = folium.IFrame(table('DC Buses', route_st, cross_st), width=width, height=height)
    popups.append(iframe)

t = folium.FeatureGroup(name='Buses')
t.add_children(MarkerCluster(locations=locations, popups=popups))
mapa.add_children(t)
Out[10]:
In [11]:
mapa.add_children(folium.LayerControl())
mapa
Out[11]:

This notebook was borrowed and slightly modified from Felipe Fernandez of python4oceanographers. The original can be found here https://nbviewer.ipython.org/url/ocefpaf.github.com/python4oceanographers/downloads/notebooks/2015-12-14-geopandas_folium.ipynb

In [ ]: